#pragma once
//文件版本
#include "../comm/util.hpp"
#include "../comm/log.hpp"

#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <fstream>
#include <cstdlib>
#include <cassert>

// 根据题目list文件，加载所有的题目信息到内存中
// model: 主要用来和数据进行交互，对外提供访问数据的接口

namespace ns_model
{
    using namespace std;
    using namespace ns_log;
    using namespace ns_util;

    // 这个结构体用来描述一个题目
    struct Question
    {
        std::string number; //题目编号，唯一
        std::string title;  //题目的标题
        std::string star;   //难度: 简单 中等 困难
        int cpu_limit;      //题目的时间要求(S)
        int mem_limit;      //题目的空间要去(KB)
        std::string desc;   //题目的描述
        std::string header; //题目预设给用户在线编辑器的代码
        std::string tail;   //题目的测试用例，需要和header拼接，形成完整代码
    };

    const std::string questins_list = "./questions/questions.list";
    const std::string questins_path = "./questions/";

    class Model
    {
    private:
        // 建立一个从题目的题号到完整题目的映射
        unordered_map<string, Question> questions;
    public:
        Model()
        {
            assert(LoadQuestionList(questins_list));
        }
        
        // 加载问题清单
            // 原本我网站上的所有题目都是存储在服务器的一个个question文件中，这非常不方便
            // 为了便于问题的组织管理，我们引入了结构体struct Question来描述一个题目
            // 我们这个函数的任务，就是先通过读question文件，将所有的问题信息都描述成一个个的结构体struct Question
            // 然后再将每个题目的题号，与该题目的struct Question组合成一个键值对，存入哈希表questions中
        bool LoadQuestionList(const string &question_list)
        {
            // 注意我们这个函数传入的参数const string &question_list，它是一个文件路径，指向的文件是一份题目清单，里面每一行就对应一个题目表项，记录着一个题目的基本信息
            // 表项的格式如下：题号+题目名称+难度+CPU限时+允许使用的最大空间，比如： 1 判断回文数 简单 1 30000

            //加载配置文件: questions/questions.list + 题目编号文件
            ifstream in(question_list);
            if(!in.is_open())
            {
                LOG(FATAL) << " 加载题库失败,请检查是否存在题库文件" << "\n";
                return false;
            }
            // 走到这里说明题库文件打开成功
            // 由于文件中一行记录着一个问题的基本信息，那我接下来就按行为单位来读取文件
            // 每读取一行，我就创建一个新的struct Question结构体，然后将该行问题的信息都存入这个结构体中
            string line;
            while(getline(in, line))
            {
                // 走到这里，我们已经把文件中getline新读取的那行信息读入了string类型的line中
                // 下面就是对line进行解析，把line中的各属性信息写入struct Question的各成员变量中

                vector<string> tokens;
                // 首先以" "为分隔符，将line分割成若干子字符串，然后将这些字符串按从左到右的顺序放入vector<string>类型的 tokens数组中
                StringUtil::SplitString(line, &tokens, " ");

                // 如果分割成的子字符串不是5个，说明这一行的格式不对，于是我们记录一条错误日志，放弃这一行的解析，直接开始下一行的解析
                if(tokens.size() != 5)
                {
                    LOG(WARNING) << "加载部分题目失败, 请检查文件格式" << "\n";
                    continue;
                }
                // 注意我们每一行问题基本信息的格式：题号+题目名称+难度+CPU限时+允许使用的最大空间，比如： 1 判断回文数 简单 1 30000
                // 走到这里，说明我们已经成功将拆分后的子字符串都存到了vector<string>类型的tokens数组中
                // tokens[0]里面存的就是题号，tokens[1]里面存的就是题目名称，依次类推
                // 下面我们就将这些信息都存入结构体中
                Question q;
                q.number = tokens[0];
                q.title = tokens[1];
                q.star = tokens[2];
                q.cpu_limit = atoi(tokens[3].c_str());
                q.mem_limit = atoi(tokens[4].c_str());

                // questins_path是题目文件所在目录的路径信息
                // 我们再根据该题目的题号，二者一拼，就可以得到存储着该题具体内容的文件的路径信息
                string path = questins_path;
                path += q.number;
                path += "/";
                // 举个例子，questins_path="./questions/"，当前行问题的题号是1号，则存储1号题目的文件路径信息就是"./questions/1/"
                // 由于我们规定好的，每个题目文件夹下有三个文件：
                    // 一个是记录着该题目描述信息的文件"desc.txt"
                    // 一个是该题网页端编辑器内预先填充的默认代码文件"header.cpp"
                    // 一个是最终跑测试用例的代码文件"tail.cpp"
                // 通过地址拼接，就可以得到这三个文件的完整路径
                // 然后我们通过ReadFile函数，读出三个文件中的信息，存入结构体的对应成员变量中
                FileUtil::ReadFile(path+"desc.txt", &(q.desc), true);
                FileUtil::ReadFile(path+"header.cpp", &(q.header), true);
                FileUtil::ReadFile(path+"tail.cpp", &(q.tail), true);

                // 到此为止，struct Question 中的所有属性，我们都已经填充完毕，这已经是一个记录着问题完整信息的结构体了
                // 下面我们就以题号为key，该结构体为value，组成一个完整的键值对，将该键值对插入到哈希表中
                questions.insert({q.number, q});
            }
            LOG(INFO) << "加载题库...成功!" << "\n";
            in.close();

            return true;
        }
        
        // 获取所有的题目
            // 在文件版本中，我网站上的所有题目都是存储在服务器的一个个question文件中
            // 构建好哈希表questions：文件中的一个个题目就被描述为一个个的结构体，每个题目与其对应的题号构成一个键值对，存储哈希表questions中
            // 执行GetAllQuestions时，我默认哈希表questions已经创建完毕，现在我要将所有的题目信息从哈希表questions中提取出来，提取到我创建的vector<Question>类型的数组中
        bool GetAllQuestions(vector<Question> *out)
        {
            // 差错处理，如果哈希表中没有键值对，说明我们获取题库失败了
            if(questions.size() == 0)
            {
                LOG(ERROR) << "用户获取题库失败" << "\n";
                return false;
            }
            // 走到这里说明questions.size() > 0，那我就遍历哈希表，取出每个键值对中的value，放到我的vector<Question>数组中
            for(const auto &q : questions){
                out->push_back(q.second); //first: key, second: value
            }
            return true;
        }

        // 获取一个题目：根据题号，在哈希表中查找题目，找到之后将题目存入输出型参数Question *q中
        bool GetOneQuestion(const std::string &number, Question *q)
        {
            // 拿着题号去哈希表里面查题目
            const auto& iter = questions.find(number);
            // 没查到就进行差错处理
            if(iter == questions.end()){
                LOG(ERROR) << "用户获取题目失败, 题目编号: " << number << "\n";
                return false;
            }
            // 查到了就将题目存入输出型参数Question *q中
            (*q) = iter->second;
            return true;
        }
        ~Model()
        {}
    };
} // namespace ns_model
